package com.nulldev.util.data.VariableCaches;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.LongAdder;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.util.random.GlobalRandom;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.internal.unsafecompat.UnsafeCompat;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.nullUtil;

public class VariableCache implements AutoCloseable {

	private static final Logger log = LoggerFactory.getLogger();
	private static final int NR_BITS = JVM.getDataWidth();
	private static final int BYTE = 8;
	private static final int WORD = NR_BITS / BYTE;
	private static final int MIN_SIZE = 16;

	private final long maximumMemorySize;
	private final LongAdder currentSize = new LongAdder();

	private static final Random fr = GlobalRandom.getFallbackRandom();

	private class CachedObject<T> {

		private volatile T object;
		private volatile long size;
		private final long object_id;

		public CachedObject(final T object) {
			this.object = object;
			this.size = sizeOf(object.getClass());
			if (object != null && object instanceof String) {
				this.size += ((String) object).length();
			} else if (object != null && object instanceof byte[]) {
				this.size += ((byte[]) object).length;
			} else if (!nullUtil.IS_ANDROID_BUILD && object != null && object instanceof java.awt.image.BufferedImage) {
				final java.awt.image.DataBuffer dataBuffer = ((java.awt.image.BufferedImage) object).getData().getDataBuffer();
				this.size += ((long) dataBuffer.getSize()) * 4l;
			} else {
				log.warn("Unknown object, guessing size: " + object);
				this.size += sizeOf(object.getClass());
			}
			this.object_id = fr.nextLong();
		}

		public CachedObject(final long object_id, final T object) {
			this.object = object;
			this.size = sizeOf(object.getClass());
			if (object != null && object instanceof String) {
				this.size += ((String) object).length();
			} else if (object != null && object instanceof byte[]) {
				this.size += ((byte[]) object).length;
			} else if (!nullUtil.IS_ANDROID_BUILD && object != null && object instanceof java.awt.image.BufferedImage) {
				final java.awt.image.DataBuffer dataBuffer = ((java.awt.image.BufferedImage) object).getData().getDataBuffer();
				this.size += ((long) dataBuffer.getSize()) * 4l;
			} else {
				log.warn("Unknown object, guessing size: " + object);
				this.size += sizeOf(object.getClass());
			}
			this.object_id = object_id;
		}

		public T get() {
			return this.object;
		}

		@SuppressWarnings("unused")
		public long size() {
			return this.size;
		}

		@SuppressWarnings("unused")
		public long id() {
			return this.object_id;
		}
	}

	private final Map<Long, CachedObject<?>> cache = FastMaps.map();

	public VariableCache(final long maxSize) {
		if (maxSize <= 0) {
			throw new UnsupportedOperationException("maxSize must be larger than 0 bytes!");
		}
		this.maximumMemorySize = maxSize;
	}

	public <T> long allocateObject(final T object) throws UnsupportedOperationException {
		final CachedObject<T> co = new CachedObject<T>(object);
		if (co.size + this.currentSize.sum() > this.maximumMemorySize) {
			throw new UnsupportedOperationException(
					"allocateObject() -> Buffer is full! [wanted=" + co.size + ",current=" + this.currentSize.sum() + ",max=" + this.maximumMemorySize + "]");
		}
		this.cache.put(co.object_id, co);
		this.currentSize.add(co.size);
		return co.object_id;
	}

	@SuppressWarnings("unchecked")
	public <T> T get(final long object_id) {
		final CachedObject<T> object = (CachedObject<T>) this.cache.get(object_id);
		if (object == null) {
			return null;
		} else {
			return object.get();
		}
	}

	private static int sizeOf(Class<?> src) {
		final List<Field> instanceFields = new LinkedList<Field>();
		do {
			if (src == Object.class)
				return MIN_SIZE;
			for (final Field f : src.getDeclaredFields()) {
				if ((f.getModifiers() & Modifier.STATIC) == 0) {
					instanceFields.add(f);
				}
			}
			src = src.getSuperclass();
		} while (instanceFields.isEmpty());
		long maxOffset = 0;
		for (final Field f : instanceFields) {
			long offset = UnsafeCompat.get().objectFieldOffset(f);
			if (offset > maxOffset)
				maxOffset = offset;
		}
		return (((int) maxOffset / WORD) + 1) * WORD;
	}

	public boolean has(final long object_id) {
		return this.cache.containsKey(object_id);
	}

	@Override
	public String toString() {
		return "VariableCache[hash=" + MathUtil.toHex(super.hashCode()) + ",size=" + this.currentSize.sum() + ",max=" + this.maximumMemorySize + "]";
	}

	public <T> long put(final long object_id, final T object) {
		final CachedObject<T> co = new CachedObject<T>(object_id, object);
		if (co.size + this.currentSize.sum() > this.maximumMemorySize) {
			throw new UnsupportedOperationException(
					"allocateObject() -> Buffer is full! [wanted=" + co.size + ",current=" + this.currentSize.sum() + ",max=" + this.maximumMemorySize + "]");
		}
		this.cache.put(co.object_id, co);
		this.currentSize.add(co.size);
		return co.object_id;
	}

	@Override
	public void close() throws Exception {
		this.cache.clear();
		this.currentSize.reset();
	}
}
